```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
  <meta name="color-scheme" content="light" />
  <title>小香香的烦恼焚烧站 · by 你的小火炉</title>
  <meta name="description" content="把考试的烦恼写下来，投入小火炉，化作好运小星星～" />
  <style>
    :root {
      --bg: #F7F2E6;
      --bg-grad-top: #FFF9F1;
      --bg-grad-btm: #F5E7D4;
      --card: #fff6e9;
      --primary: #FF7F50;
      --primary-2: #FF935F;
      --accent: #FFD36A;
      --text: #5C4634;
      --muted: #7A5A43;
      --shadow: 0 14px 36px rgba(139, 69, 19, 0.18);
      --radius: 20px;
      --focus: rgba(255, 127, 80, .34);
      --maxw: 1200px;
    }

    html, body {
      height: 100%;
      font-family: ui-rounded, "SF Pro Rounded", "SF Pro Text", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Nunito", "Quicksand", system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
      color: var(--text);
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-rendering: optimizeLegibility;
      background: radial-gradient(1200px 500px at 50% -10%, var(--bg-grad-top), transparent 70%),
                  radial-gradient(800px 400px at 80% 120%, rgba(255,213,160,.35), transparent 70%),
                  linear-gradient(var(--bg-grad-top), var(--bg-grad-btm));
      position: relative;
    }

    body::before {
      content: "";
      position: fixed; inset: 0; pointer-events: none; z-index: 0;
      background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" viewBox="0 0 100 100"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" stitchTiles="stitch"></feTurbulence><feColorMatrix type="saturate" values="0"></feColorMatrix><feComponentTransfer><feFuncA type="table" tableValues="0 0 .06 0"></feFuncA></feComponentTransfer></filter><rect width="100" height="100" filter="url(%23n)" opacity="0.35"/></svg>');
      opacity: .35;
      mix-blend-mode: multiply;
    }

    * { box-sizing: border-box; }
    a { color: var(--primary); }

    .wrap { max-width: var(--maxw); margin: 0 auto; padding: calc(16px + env(safe-area-inset-top)) 18px calc(28px + env(safe-area-inset-bottom)); position: relative; z-index: 1; }

    header { text-align: center; margin: 14px auto 22px; }

    h1 { margin: 6px 0 8px; font-size: clamp(28px, 3.8vw, 48px); letter-spacing: .4px; line-height: 1.12; font-weight: 900; color: #6c4c34; }
    .title-underline { width: 180px; height: 10px; margin: 8px auto 0; background: radial-gradient(closest-side, rgba(255,174,89,.6), rgba(255,174,89,.05) 70%, transparent); border-radius: 999px; filter: blur(4px); }

    .subtitle { font-size: clamp(14px, 1.8vw, 18px); color: #7a5a43; opacity: .95; margin-top: 8px; }

    .desc { margin: 12px auto 18px; max-width: 820px; font-size: clamp(14px, 1.9vw, 18px); line-height: 1.7; color: var(--muted); background: #fff7efcc; border: 1px dashed #efc99a; border-radius: 16px; padding: 14px 16px; box-shadow: var(--shadow); }

    .toolbar { display: flex; gap: 10px; justify-content: center; align-items: center; margin: 10px auto 18px; flex-wrap: wrap; }
    .chip { border-radius: 999px; padding: 9px 14px; font-size: 14px; cursor: pointer; border: 1px solid #f2cfab; background: #fff7ec; color: #7b5a45; box-shadow: 0 6px 16px rgba(0,0,0,.06); -webkit-tap-highlight-color: transparent; user-select: none; transition: transform .15s ease, background .2s, border-color .2s; }
    .chip:hover { transform: translateY(-1px); }
    .chip[aria-pressed="true"] { background: #ffe3bf; border-color: #f2bd7d; }

    main { display: grid; gap: 20px; align-items: start; grid-template-columns: 1fr; }
    @media (min-width: 960px) { main { grid-template-columns: 1.15fr .85fr; } }

    .card { background: var(--card); border-radius: var(--radius); padding: 16px; box-shadow: var(--shadow); position: relative; overflow: clip; }

    .label { display: block; font-size: 14px; color: #7a5a43; margin-bottom: 8px; }

    .input-wrap { position: relative; }

    textarea.big-input { width: 100%; min-height: clamp(190px, 32vh, 300px); border: 1.5px solid #e8d8c4; outline: none; padding: 16px 16px 52px; border-radius: 18px; resize: vertical; background: #fff8ed; color: var(--text); line-height: 1.65; font-size: 16px; transition: box-shadow .25s ease, transform .2s ease, opacity .25s ease; }
    textarea.big-input::placeholder { color: #a0866d; opacity: .7; }
    textarea.big-input:focus { box-shadow: 0 0 0 4px var(--focus), var(--shadow); transform: translateY(-1px); }
    textarea.big-input.vanish { opacity: 0; }

    .actions { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; gap: 12px; flex-wrap: wrap; }

    .fire-btn { appearance: none; border: none; cursor: pointer; padding: 14px 18px; min-width: 190px; border-radius: 999px; font-weight: 900; letter-spacing: .3px; font-size: 16px; color: white; background: linear-gradient(135deg, var(--primary), var(--primary-2)); box-shadow: 0 12px 26px rgba(255, 127, 80, .36), inset 0 -2px 0 rgba(0,0,0,.08); transition: transform .16s ease, box-shadow .22s ease, filter .22s ease; position: relative; overflow: hidden; }
    .fire-btn:hover { transform: translateY(-1px) scale(1.02); filter: brightness(1.03); }
    .fire-btn:active { transform: translateY(0) scale(.98); }
    .fire-btn:disabled { opacity: .6; cursor: not-allowed; }
    .fire-btn::after { content: ""; position: absolute; inset: 0; background: radial-gradient(400px 120px at var(--mx,50%) 20%, rgba(255,255,255,.4), transparent 40%); opacity: 0; transition: opacity .2s ease; }
    .fire-btn:hover::after { opacity: .85; }

    .helper { font-size: 13px; color: #85654e; opacity: .95; }

    .quote { position: absolute; left: 8px; right: 8px; top: 50%; transform: translateY(-50%) scale(.98); text-align: center; background: #fffdf7; border: 1px solid #f2e4cd; border-radius: 16px; padding: 18px 16px; font-size: 20px; font-weight: 800; color: #6a4a36; line-height: 1.7; box-shadow: 0 16px 28px rgba(0,0,0,.08); opacity: 0; pointer-events: none; transition: opacity .5s cubic-bezier(.22,.8,.3,1), transform .5s cubic-bezier(.22,.8,.3,1); }
    .quote.show { opacity: 1; transform: translateY(-50%) scale(1); }

    .letters-wrap { position: fixed; pointer-events: none; z-index: 9998; left: 0; top: 0; }
    .letter { display: inline-block; font-size: 16px; line-height: 1; color: #6a5a46; will-change: transform, opacity; }

    .cloud { position: fixed; width: 110px; height: 72px; z-index: 9999; filter: url(#gooey) blur(0.25px); }
    .blob { position: absolute; border-radius: 50%; background: radial-gradient(circle at 30% 35%, #ffffff, #ececec 50%, #d6d6d6 90%); box-shadow: inset 0 2px 4px rgba(255,255,255,.7), inset 0 -2px 6px rgba(0,0,0,.08), 0 8px 18px rgba(0,0,0,.10); }

    @keyframes riseAndGather { 0% { transform: translate(var(--sx), var(--sy)) scale(1); opacity: 1; } 100% { transform: translate(var(--tx), var(--ty)) scale(.5); opacity: 0; } }

    .stove-card { position: sticky; top: 14px; }
    .stove-wrap { background: var(--card); border-radius: var(--radius); padding: 12px; box-shadow: var(--shadow); }
    .stove-stage { position: relative; }
    .stove-glow { position: absolute; inset: -16px; border-radius: 30px; filter: blur(24px); z-index: 0; opacity: .36; background: radial-gradient(closest-side, rgba(255,121,60,.36), rgba(255,214,119,.18), transparent 70%); animation: glow 3s ease-in-out infinite; }
    @keyframes glow { 0%, 100% { opacity: .32 } 50% { opacity: .5 } }

    .stove { position: relative; width: 100%; max-width: 460px; margin: 10px auto; display: block; }

    .flame { transform-origin: center bottom; animation: flicker 1.1s ease-in-out infinite; }
    .flame.small { animation-duration: 1.5s; opacity: .85; }
    @keyframes flicker { 0%,100% { transform: scaleY(1) translateY(0) } 50% { transform: scaleY(1.06) translateY(-2px) } }
    .stove-idle .flame { filter: drop-shadow(0 0 7px rgba(255, 206, 120, .55)); }
    .stove-working .flame { animation-duration: .58s; filter: drop-shadow(0 0 16px rgba(255, 196, 0, .98)); }

    /* Face states */
    .face-group { transition: opacity .28s ease; }
    .face-happy { opacity: 0; }
    .stove-working .face-happy { opacity: 1; }
    .stove-working .face-idle { opacity: 0; }

    .chimney-stars { position: absolute; pointer-events: none; left: 50%; transform: translateX(-50%); top: 0; width: 110px; height: 190px; overflow: visible; }
    .star { position: absolute; font-size: 18px; opacity: 0; }
    @keyframes floatUp { 0% { transform: translate(0,0) scale(.85) rotate(0deg); opacity: 0; } 8% { opacity: 1 } 100% { transform: translate(var(--dx), -170px) scale(1.25) rotate(12deg); opacity: 0; } }
    /* Hearts */
    .heart { position: absolute; font-size: 22px; opacity: 0; filter: drop-shadow(0 2px 6px rgba(255,0,100,.25)); will-change: transform, opacity; }
    @keyframes floatHeart { 0% { transform: translate(0,0) scale(.8) rotate(0deg); opacity: 0; } 10% { opacity: 1; } 60% { transform: translate(calc(var(--hx) * .6), -120px) scale(1.25) rotate(-8deg);} 100% { transform: translate(var(--hx), -220px) scale(1.5) rotate(-14deg); opacity: 0; } }
    @keyframes popHeart { 0% { transform: translate(0,0) scale(.4) rotate(0deg); opacity: 0; } 18% { opacity: 1; transform: translate(calc(var(--bx) * .5), calc(var(--by) * .5)) scale(1.2) rotate(6deg);} 100% { transform: translate(var(--bx), var(--by)) scale(1) rotate(12deg); opacity: 0; } }
    @keyframes swirlHeart { 0% { transform: translate(0,0) scale(.8) rotate(0deg); opacity: 0;} 15% { opacity: 1;} 100% { transform: translate(calc(var(--hx) + 20px), -260px) rotate(40deg) scale(1.3); opacity: 0;} }

    @media (prefers-reduced-motion: reduce) { * { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }

    .sr-only { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
    footer { text-align: center; margin-top: 26px; font-size: 13px; color: #7a5a43; opacity: .95; }
  </style>

  <!-- SVG filters for gooey -->
  <svg width="0" height="0" style="position:absolute">
    <defs>
      <filter id="gooey">
        <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur" />
        <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -8" result="goo" />
        <feComposite in="SourceGraphic" in2="goo" operator="atop"/>
      </filter>
    </defs>
  </svg>
</head>
<body>
  <div class="wrap">
    <header>
      <h1>小香香的专属烦恼焚烧站</h1>
      <div class="title-underline" aria-hidden="true"></div>
      <div class="subtitle">由你的小火炉特别赞助 ❤️</div>
      <p class="desc">把所有关于考试的焦虑、压力和不开心，都写在下面的盒子里吧！小火炉会帮它们统统烧掉，变成好运气～<br/>提示：可在下方开启“暖暖声音”，更有临场感（iOS 需你主动点一下才能播放）。</p>
      <div class="toolbar" role="group" aria-label="偏好设置">
        <button class="chip" id="soundToggle" aria-pressed="false">🔊 开启暖暖声音</button>
        <button class="chip" id="motionToggle" aria-pressed="false">🌿 减少动态</button>
      </div>
    </header>

    <main>
      <!-- Left: input -->
      <section class="card" id="inputCard" aria-labelledby="lbl">
        <label id="lbl" class="label" for="worries">写下今天的烦恼：</label>
        <div class="input-wrap">
          <textarea id="worries" class="big-input" placeholder="任何烦恼，皆可焚烧..." aria-describedby="hint"></textarea>
          <div id="quote" class="quote" role="status" aria-live="polite"></div>
        </div>
        <div class="actions">
          <div class="helper" id="hint">按 Enter 或点击按钮投喂；支持 ⌘/Ctrl + Enter 快捷键。</div>
          <div style="flex:1"></div>
          <button class="fire-btn" id="burnBtn">投入小火炉</button>
        </div>
      </section>

      <!-- Right: stove -->
      <aside class="stove-card">
        <div class="stove-wrap">
          <div class="stove-stage">
            <div class="stove-glow" aria-hidden="true"></div>

            <!-- Chimney stars container -->
            <div class="chimney-stars" id="starsHost" aria-hidden="true"></div>
            <div class="chimney-stars" id="heartsHost" aria-hidden="true" style="pointer-events:none"></div>

            <!-- Cute stove SVG -->
            <svg class="stove stove-idle" id="stove" viewBox="0 0 520 520" role="img" aria-labelledby="stoveTitle stoveDesc">
              <title id="stoveTitle">小火炉</title>
              <desc id="stoveDesc">一个有笑眯眯眼睛的可爱小火炉，帮你把烦恼烧掉</desc>
              <!-- body -->
              <defs>
                <filter id="innerShadow" x="-50%" y="-50%" width="200%" height="200%">
                  <feOffset dx="0" dy="2"/>
                  <feGaussianBlur stdDeviation="4" result="offset-blur"/>
                  <feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"/>
                  <feFlood flood-color="#d95b2a" flood-opacity="0.4" result="color"/>
                  <feComposite operator="in" in="color" in2="inverse" result="shadow"/>
                  <feComposite operator="over" in="shadow" in2="SourceGraphic"/>
                </filter>
                <radialGradient id="bodyGrad" cx="50%" cy="35%" r="70%">
                  <stop offset="0%" stop-color="#FF8C5A"/>
                  <stop offset="60%" stop-color="#FF7F50"/>
                  <stop offset="100%" stop-color="#e15e2a"/>
                </radialGradient>
                <linearGradient id="windowGrad" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="0%" stop-color="#3a1c0b"/>
                  <stop offset="100%" stop-color="#2c1507"/>
                </linearGradient>
                <radialGradient id="cheekGrad" cx="50%" cy="50%" r="50%">
                  <stop offset="0%" stop-color="#ffa6a6"/>
                  <stop offset="100%" stop-color="#ff7f7f"/>
                </radialGradient>
                <linearGradient id="chimGrad" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="0%" stop-color="#8a4e2e"/>
                  <stop offset="100%" stop-color="#6d3b22"/>
                </linearGradient>
                <radialGradient id="flameCore" cx="50%" cy="60%" r="60%">
                  <stop offset="0%" stop-color="#fff2a8"/>
                  <stop offset="45%" stop-color="#ffd85e"/>
                  <stop offset="100%" stop-color="#ff8a34"/>
                </radialGradient>
              </defs>

              <!-- chimney -->
              <g transform="translate(355,10)">
                <rect x="0" y="0" width="60" height="140" rx="16" fill="url(#chimGrad)" />
                <rect x="-10" y="130" width="80" height="20" rx="10" fill="#8a4e2e" />
              </g>

              <!-- legs -->
              <g opacity=".9">
                <rect x="120" y="420" width="40" height="30" rx="10" fill="#7a3e22" />
                <rect x="360" y="420" width="40" height="30" rx="10" fill="#7a3e22" />
              </g>

              <!-- body -->
              <rect x="70" y="100" width="380" height="340" rx="40" fill="url(#bodyGrad)" filter="url(#innerShadow)" />

              <!-- window (drawn before face) moved down to avoid overlap with mouth) -->
              <g id="window" transform="translate(140,260)">
                <rect x="0" y="0" width="240" height="160" rx="16" fill="url(#windowGrad)" stroke="#3b1f10" stroke-width="3" />
                <g id="flames">
                  <ellipse class="flame small" cx="80" cy="100" rx="18" ry="34" fill="url(#flameCore)" />
                  <ellipse class="flame" cx="120" cy="100" rx="22" ry="42" fill="url(#flameCore)" />
                  <ellipse class="flame small" cx="160" cy="100" rx="18" ry="34" fill="url(#flameCore)" />
                  <ellipse class="flame" cx="200" cy="100" rx="24" ry="48" fill="url(#flameCore)" />
                </g>
              </g>
              <rect x="146" y="266" width="228" height="148" rx="12" fill="transparent" stroke="rgba(255,255,255,.26)" stroke-width="5" />

              <!-- face (after window to avoid occlusion) -->
              <g class="face-group face-idle">
                <circle cx="190" cy="188" r="10" fill="#4a2b17" />
                <circle cx="330" cy="188" r="10" fill="#4a2b17" />
                <path d="M210,218 C240,240 280,240 310,218" fill="none" stroke="#4a2b17" stroke-width="6" stroke-linecap="round"/>
                <circle cx="160" cy="208" r="12" fill="url(#cheekGrad)" opacity=".9"/>
                <circle cx="360" cy="208" r="12" fill="url(#cheekGrad)" opacity=".9"/>
              </g>
              <g class="face-group face-happy">
                <path d="M175,186 C185,196 195,196 205,186" fill="none" stroke="#4a2b17" stroke-width="6" stroke-linecap="round"/>
                <path d="M315,186 C325,196 335,196 345,186" fill="none" stroke="#4a2b17" stroke-width="6" stroke-linecap="round"/>
                <path d="M210,222 C240,255 280,255 310,222" fill="none" stroke="#4a2b17" stroke-width="7" stroke-linecap="round"/>
                <circle cx="158" cy="210" r="13" fill="url(#cheekGrad)" opacity="1"/>
                <circle cx="362" cy="210" r="13" fill="url(#cheekGrad)" opacity="1"/>
              </g>
            </svg>
          </div>
        </div>
      </aside>
    </main>

    <footer>
      <div>© <span id="year"></span> 小火炉给小香香的专属小站 · 本地运行，无数据上传</div>
    </footer>
  </div>

  <div class="sr-only" aria-live="polite" id="live"></div>

  <script>
    // --- Utilities ---
    const $ = (sel, root = document) => root.querySelector(sel);
    const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));

    const state = {
      working: false,
      reducedMotion: false,
      soundOn: false,
      audio: { ctx: null, master: null, fireBed: null, crackleTimer: null, pan: null, targetVol: 0.2 },
      DUR_GATHER_MIN: 900,
      DUR_GATHER_MAX: 1200,
      DUR_CLOUD_BREATH: 1400,
      DUR_ARC: 1600,
      QUOTE_STAY: 3500
    };

    const quotes = [
      "你看，烦恼烧掉就没啦！小火炉一直在给你输送能量哦！",
      "呼～负能量-100！好运气+100！去休息一下吧，我的小香香最棒了！",
      "焚烧完毕！据小火炉观测，你的大脑内存已清理，学习效率即将翻倍！",
      "别怕，有我呢。考完试我们去吃大餐！这叫‘能量守恒定律’！",
      "刚刚那个是‘笨蛋烦恼’，已经被我烧成灰了。现在只剩下‘聪明天才’的小香香啦！",
      "小香香～烦恼交给小火炉啦(づ￣ ³￣)づ♡",
      "看～火星蹦蹦跳跳，替你把焦虑吹散啦！",
      "小火炉把担心揉成棉花糖咯～甜甜的！",
      "认真可爱的你，值得被小星星围绕呀～",
      "成功投喂！小火炉为小香香加火力啦！",
      "呜哇～这团乌云被烤成金色好运啦！",
      "让我抱一下，再送你一颗大爱心～❤",
      "你一投喂，我就更爱你一分～多给我几次机会吧～",
      "小香香～烦恼交给小火炉啦",
      "看～火星蹦蹦跳跳，替你把焦虑吹散啦！",
      "小火炉把担心揉成棉花糖咯～甜甜的！",
      "成功投喂！小火炉为小香香加火力啦！",
      "呜哇～这团乌云被烤成金色好运啦！",
      "让我抱一下，再送你一颗大爱心～❤",
      "把你紧紧装进口袋里～不让焦虑靠近！",
      "敲可爱的小香香，继续向前冲鸭！",
      "一切会顺顺的～小火炉替你护航！",
      "抱一抱就好啦～你已经很可爱很努力啦！",
      "伸手～把好运塞进你掌心里啰～",
      "你一笑～小火炉心都软啦(灬º‿º灬)♡",
      "给你专属的“抱抱券”～随时兑换！",
    ];

    // DOM elements
    const burnBtn = $('#burnBtn');
    const textarea = $('#worries');
    const card = $('#inputCard');
    const quoteEl = $('#quote');
    const stove = $('#stove');
    const starsHost = $('#starsHost');
    const heartsHost = $('#heartsHost');
    const live = $('#live');
    const soundToggle = $('#soundToggle');
    const motionToggle = $('#motionToggle');

    const ease = 'cubic-bezier(.22,.8,.3,1)';

    // --- Improved FIRE AUDIO (granular + bands + subtle room) ---
    function initAudio() {
      if (state.audio.ctx) return;
      const ctx = new (window.AudioContext || window.webkitAudioContext)();
      const master = ctx.createGain();
      master.gain.value = state.audio.targetVol;
      master.connect(ctx.destination);
      const pan = (ctx.createStereoPanner ? ctx.createStereoPanner() : ctx.createGain());
      if (pan.pan) pan.pan.value = 0;
      pan.connect(master);

      // Fire bed (pink-ish noise + EQ)
      const bufferSize = 2 * ctx.sampleRate;
      const noiseBuffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
      const data = noiseBuffer.getChannelData(0);
      let b0=0,b1=0,b2=0,b3=0,b4=0,b5=0,b6=0;
      for (let i = 0; i < bufferSize; i++) {
        const white = Math.random()*2-1;
        b0 = 0.99886*b0 + white*0.0555179;
        b1 = 0.99332*b1 + white*0.0750759;
        b2 = 0.96900*b2 + white*0.1538520;
        b3 = 0.86650*b3 + white*0.3104856;
        b4 = 0.55000*b4 + white*0.5329522;
        b5 = -0.7616*b5 - white*0.0168980;
        data[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + white*0.5362) * 0.10;
        b6 = white*0.115926;
      }
      const bed = ctx.createBufferSource(); bed.buffer = noiseBuffer; bed.loop = true;
      const hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=70; hp.Q.value=0.7;
      const lp = ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=1800; lp.Q.value=0.7;
      const shelf = ctx.createBiquadFilter(); shelf.type='lowshelf'; shelf.frequency.value=220; shelf.gain.value=5;
      const bedGain = ctx.createGain(); bedGain.gain.value = 0.22;

      const lfo = ctx.createOscillator(); lfo.type='sine'; lfo.frequency.value = 0.35;
      const lfoGain = ctx.createGain(); lfoGain.gain.value = 0.06;
      lfo.connect(lfoGain).connect(bedGain.gain);

      bed.connect(hp).connect(shelf).connect(lp).connect(bedGain).connect(pan);
      lfo.start();

      state.audio = { ctx, master, fireBed: { src: bed, gain: bedGain }, crackleTimer: null, pan, targetVol: 0.2 };
    }

    function startFire() {
      if (!state.audio.ctx) initAudio();
      const { ctx, fireBed } = state.audio;
      if (ctx.state === 'suspended') ctx.resume();
      if (!fireBed.src.started) { fireBed.src.started = true; fireBed.src.start(); }
      startCrackles();
      startPanDrift();
    }

    function startPanDrift(){
      const { pan } = state.audio;
      if (!pan || !pan.pan) return;
      let t = 0;
      function tick(){
        if(!state.soundOn) { requestAnimationFrame(tick); return; }
        t += 0.002; pan.pan.value = Math.sin(t)*0.2; requestAnimationFrame(tick);
      }
      requestAnimationFrame(tick);
    }

    function stopFire() {
      const { crackleTimer } = state.audio;
      if (crackleTimer) { clearInterval(crackleTimer); state.audio.crackleTimer = null; }
    }

    function startCrackles() {
      const { ctx, master } = state.audio;
      if (state.audio.crackleTimer) return;
      state.audio.crackleTimer = setInterval(() => {
        if (!state.soundOn) return;
        // granular crackles: groups & single pops
        const chance = 0.75;
        if (Math.random() < chance) {
          const grains = 1 + Math.floor(Math.random()*4);
          for (let g=0; g<grains; g++) {
            const dur = 0.015 + Math.random()*0.045;
            const src = ctx.createBufferSource();
            const b = ctx.createBuffer(1, Math.ceil(dur*ctx.sampleRate), ctx.sampleRate);
            const arr = b.getChannelData(0);
            for (let i=0;i<arr.length;i++) {
              const t=i/arr.length; // exponential decay envelope + tiny overshoot
              arr[i] = (Math.random()*2-1) * Math.pow(1 - t, 2.4) * (1 + 0.2*Math.sin(i*0.5));
            }
            src.buffer = b;
            const bp = ctx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value = 1200 + Math.random()*2600; bp.Q.value = 8;
            const gNode = ctx.createGain(); gNode.gain.value = 0.16 + Math.random()*0.18;
            src.connect(bp).connect(gNode).connect(master);
            const when = ctx.currentTime + g*0.012 + Math.random()*0.008;
            src.start(when);
          }
        }
      }, 85);
    }

    function whoosh() {
      if (!state.soundOn) return;
      const { ctx, master } = state.audio;
      const src = ctx.createBufferSource();
      const len = Math.ceil(0.45*ctx.sampleRate);
      const b = ctx.createBuffer(1, len, ctx.sampleRate);
      const arr = b.getChannelData(0);
      for (let i = 0; i < arr.length; i++) {
        const t = i / len;
        arr[i] = (Math.random()*2-1) * (Math.exp(-5*t));
      }
      src.buffer = b;
      const lp = ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value= 2200;
      const g = ctx.createGain(); g.gain.value=0.26;
      const now = ctx.currentTime;
      g.gain.setValueAtTime(0.0, now);
      g.gain.linearRampToValueAtTime(0.26, now+0.03);
      g.gain.exponentialRampToValueAtTime(0.001, now+0.45);
      src.connect(lp).connect(g).connect(master);
      src.start();
    }

    function ding() {
      if (!state.soundOn) return;
      const { ctx, master } = state.audio;
      const o = ctx.createOscillator(); const g = ctx.createGain();
      o.type='triangle'; o.frequency.value=1244; const now=ctx.currentTime;
      g.gain.setValueAtTime(0.0, now);
      g.gain.linearRampToValueAtTime(0.28, now+0.01);
      g.gain.exponentialRampToValueAtTime(0.0001, now+0.42);
      o.connect(g).connect(master); o.start(now); o.stop(now+0.45);
    }

    // --- Stars ---
    function spawnStars(n = 6) {
      for (let i = 0; i < n; i++) {
        const s = document.createElement('span'); s.className = 'star'; s.textContent = '✨';
        s.style.left = 30 + Math.random()*50 + 'px';
        s.style.setProperty('--dx', (Math.random()*40-20) + 'px');
        const delay = i*90 + Math.random()*120; const dur = 1500 + Math.random()*900;
        s.style.animation = `floatUp ${dur}ms ${delay}ms ${ease} forwards`;
        starsHost.appendChild(s);
        setTimeout(() => s.remove(), dur + delay + 60);
      }
    }

    // --- Hearts (强化：长尾+二次补发+爆裂散射) ---
    function spawnHearts(n = 18, {spread=70, long=false} = {}) {
      const baseLeft = 30; // relative to heartsHost width
      for (let i = 0; i < n; i++) {
        const h = document.createElement('span');
        h.className = 'heart';
        h.textContent = Math.random() < 0.5 ? '❤️' : (Math.random()<0.8 ? '💖' : '💗');
        h.style.left = baseLeft + Math.random()*50 + 'px';
        const hx = (Math.random()*spread - spread/2) + 'px';
        h.style.setProperty('--hx', hx);
        const delay = i*70 + Math.random()*90; const dur = (long? 2200: 1800) + Math.random()*900;
        const size = 18 + Math.random()*18; h.style.fontSize = size + 'px';
        const animName = Math.random() < 0.25 ? 'swirlHeart' : 'floatHeart';
        h.style.animation = `${animName} ${dur}ms ${delay}ms ${ease} forwards`;
        heartsHost.appendChild(h);
        setTimeout(() => h.remove(), dur + delay + 120);
      }
    }

    function burstHearts(count = 12) {
      const rect = heartsHost.getBoundingClientRect();
      const cx = rect.width/2, cy = 20;
      for (let i=0;i<count;i++) {
        const b = document.createElement('span'); b.className = 'heart'; b.textContent = Math.random()<0.5?'❤️':'💖';
        b.style.left = cx + 'px'; b.style.top = cy + 'px';
        const angle = (Math.random()*Math.PI*2);
        const dist = 40 + Math.random()*90;
        b.style.setProperty('--bx', Math.cos(angle)*dist + 'px');
        b.style.setProperty('--by', Math.sin(angle)*dist - 40 + 'px');
        const dur = 900 + Math.random()*600; const delay = Math.random()*80;
        b.style.animation = `popHeart ${dur}ms ${delay}ms ${ease} forwards`;
        const s = 16 + Math.random()*16; b.style.fontSize = s+'px';
        heartsHost.appendChild(b);
        setTimeout(()=> b.remove(), dur + delay + 80);
      }
    }

    // --- Stove state ---
    function setStoveWorking(working) {
      stove.classList.toggle('stove-working', working);
      stove.classList.toggle('stove-idle', !working);
    }

    // --- 计算炉窗入口的屏幕坐标（iOS 优先使用 SVG 变换，回退 DOMRect） ---
    function getWindowTargetPoint(){
      const win = document.getElementById('window');
      const svg = document.getElementById('stove');
      const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
                 || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);

      try {
        if (win && svg && typeof win.getBBox === 'function'
            && typeof win.getScreenCTM === 'function'
            && typeof svg.createSVGPoint === 'function') {
          const bbox = win.getBBox();
          const pt = svg.createSVGPoint();
          pt.x = bbox.x + bbox.width * 0.50;  // 炉窗水平中心
          pt.y = bbox.y + bbox.height * 0.34; // 垂直偏下靠近入口
          const m = win.getScreenCTM();
          const p = pt.matrixTransform(m);    // 转为屏幕坐标
          return { x: p.x, y: p.y + (isIOS ? 10 : 0) };
        }
      } catch(_) { /* 忽略，走回退 */ }

      const r = win.getBoundingClientRect();
      return { x: r.left + r.width * 0.5, y: r.top + r.height * 0.32 + 16 };
    }

    // --- LETTERS to CLOUD animation ---
    function lettersToCloud(startRect) {
      const wrap = document.createElement('div');
      wrap.className = 'letters-wrap';
      wrap.style.left = startRect.left + 'px';
      wrap.style.top = startRect.top + 'px';
      wrap.style.width = startRect.width + 'px';
      wrap.style.height = startRect.height + 'px';

      const text = textarea.value; // copy BEFORE clear
      const letters = Array.from(text);
      const centerX = startRect.width/2; const centerY = startRect.height/2 - 6;
      const frag = document.createDocumentFragment();
      const lineHeight = 24; let x=0, y=0;
      letters.forEach((ch, idx) => {
        const span = document.createElement('span');
        span.className = 'letter'; span.textContent = ch;
        const col = (idx % 24); const row = Math.floor(idx / 24);
        x = 10 + col*14; y = 10 + row*lineHeight*0.72;
        span.style.position='absolute'; span.style.left = x+'px'; span.style.top = y+'px';
        const tx = (centerX - x - 6) + (Math.random()*8-4);
        const ty = (centerY - y - 6) + (Math.random()*8-4);
        span.style.setProperty('--sx', '0px'); span.style.setProperty('--sy', '0px');
        span.style.setProperty('--tx', tx+'px'); span.style.setProperty('--ty', ty+'px');
        const delay = 60 + Math.random()*220; const dur = state.DUR_GATHER_MIN + Math.random()*(state.DUR_GATHER_MAX - state.DUR_GATHER_MIN);
        span.style.animation = `riseAndGather ${dur}ms ${delay}ms cubic-bezier(.22,.8,.3,1) forwards`;
        frag.appendChild(span);
      });
      wrap.appendChild(frag);
      document.body.appendChild(wrap);

      const cloud = document.createElement('div'); cloud.className = 'cloud';
      const cx = startRect.left + centerX; const cy = startRect.top + centerY;
      cloud.style.left = cx + 'px'; cloud.style.top = cy + 'px'; cloud.style.transform = 'translate(-50%,-50%)';
      const blobs = 7 + Math.floor(Math.random()*3);
      for (let i=0;i<blobs;i++) {
        const b = document.createElement('div'); b.className = 'blob';
        const w = 40 + Math.random()*28; const h = 30 + Math.random()*20;
        b.style.width = w+'px'; b.style.height = h+'px';
        const ang = Math.random()*Math.PI*2; const rad = 16 + Math.random()*16;
        const bx = Math.cos(ang)*rad; const by = Math.sin(ang)*rad;
        b.style.left = 45 + bx + 'px'; b.style.top = 28 + by + 'px';
        cloud.appendChild(b);
      }
      document.body.appendChild(cloud);

      // slower breathing
      cloud.animate([
        { transform: 'translate(-50%,-50%) scale(1)' },
        { transform: 'translate(-50%,-50%) scale(1.1)' },
        { transform: 'translate(-50%,-50%) scale(1)' }
      ], { duration: state.DUR_CLOUD_BREATH, iterations: 2, easing: 'cubic-bezier(.22,.8,.3,1)' });

      return { wrap, cloud };
    }

    async function arcCloudToStove(cloud, startX, startY, endX, endY) {
      const apexX = (startX + endX)/2 + (Math.random()*60-30);
      const apexY = Math.min(startY, endY) - 160;
      const keyframes = [
        { transform: 'translate(-50%,-50%) scale(1)' },
        { offset: .55, transform: `translate(${apexX - startX - 55}px, ${apexY - startY - 36}px) scale(1.08)` },
        { transform: `translate(${endX - startX - 55}px, ${endY - startY - 36}px) scale(.72)` }
      ];
      const anim = cloud.animate(keyframes, { duration: state.DUR_ARC, easing: 'cubic-bezier(.12,.9,.12,1)', fill: 'forwards' });
      await anim.finished.catch(()=>{});
    }

    // --- Main flow ---
    async function burnWorries() {
      if (state.working) return;
      const text = textarea.value.trim();
      if (!text) {
        textarea.animate([{ transform: 'translateX(0)' }, { transform: 'translateX(-3px)' }, { transform: 'translateX(3px)' }, { transform: 'translateX(0)' }], { duration: 200, easing: 'ease-in-out' });
        textarea.focus(); return;
      }

      state.working = true; textarea.disabled = true; burnBtn.disabled = true;

      const rect = textarea.getBoundingClientRect();

      // 立即隐藏源文字避免重叠，但复制动画内容
      textarea.classList.add('vanish');

      let lettersWrap, cloud;
      if (!state.reducedMotion) {
        ({ wrap: lettersWrap, cloud } = lettersToCloud(rect));
      }

      // 略等后清空文本
      setTimeout(() => { textarea.value = ''; }, 120);

      // 计算目标（iOS 用 SVG 变换更稳）
      const { x: endX, y: endY } = getWindowTargetPoint();

      if (!state.reducedMotion) {
        await new Promise(r => setTimeout(r, 450)); // 等字母汇聚
        await arcCloudToStove(cloud, rect.left + rect.width/2, rect.top + rect.height/2, endX, endY);
      } else {
        // 简化动效
        const ghost = document.createElement('div'); ghost.style.position='fixed'; ghost.style.left=rect.left+'px'; ghost.style.top=rect.top+'px'; ghost.style.width='24px'; ghost.style.height='24px'; ghost.style.borderRadius='50%'; ghost.style.background='#d6d6d6'; document.body.appendChild(ghost);
        const anim = ghost.animate([{ transform: 'translate(0,0) scale(1)' }, { transform: `translate(${endX-rect.left}px, ${endY-rect.top}px) scale(.6)` }], { duration: 500, easing: ease }); await anim.finished.catch(()=>{}); ghost.remove();
      }

      // 火炉反应 + 庆祝特效
      setStoveWorking(true); whoosh();
      spawnStars(9 + Math.floor(Math.random()*5));
      spawnHearts(24 + Math.floor(Math.random()*8), {spread: 100, long: true});
      setTimeout(()=> spawnHearts(14 + Math.floor(Math.random()*6), {spread: 120, long: false}), 220);
      burstHearts(16 + Math.floor(Math.random()*8));
      setTimeout(ding, 120);

      // 清理云与字母
      if (cloud) cloud.remove(); if (lettersWrap) lettersWrap.remove();

      // 展示安慰语（停留 3.5s）
      const q = quotes[Math.floor(Math.random()*quotes.length)];
      quoteEl.textContent = q; quoteEl.classList.add('show'); live.textContent = '焚烧完毕：' + q;

      await new Promise(r => setTimeout(r, 1500));
      setStoveWorking(false);

      await new Promise(r => setTimeout(r, state.QUOTE_STAY));
      quoteEl.classList.remove('show');

      // 复位
      textarea.classList.remove('vanish'); textarea.disabled = false; burnBtn.disabled = false; textarea.focus();
      state.working = false;
    }

    // --- Event wiring ---
    burnBtn.addEventListener('pointermove', (e) => {
      const rect = burnBtn.getBoundingClientRect();
      const x = ((e.clientX - rect.left) / rect.width) * 100; burnBtn.style.setProperty('--mx', x + '%');
    });
    burnBtn.addEventListener('click', burnWorries);
    textarea.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); burnWorries(); }
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'enter') { e.preventDefault(); burnWorries(); }
    });

    soundToggle.addEventListener('click', async () => {
      const pressed = soundToggle.getAttribute('aria-pressed') === 'true';
      const next = !pressed; soundToggle.setAttribute('aria-pressed', String(next)); state.soundOn = next;
      if (!state.audio.ctx) initAudio();
      await state.audio.ctx.resume();
      // Smooth volume ramp
      const g = state.audio.master.gain; const now = state.audio.ctx.currentTime;
      g.cancelScheduledValues(now);
      if (next) {
        startFire();
        g.setValueAtTime(g.value, now); g.linearRampToValueAtTime(state.audio.targetVol, now + 0.15);
        soundToggle.textContent = '🔈 关闭暖暖声音';
      } else {
        g.setValueAtTime(g.value, now); g.linearRampToValueAtTime(0.0, now + 0.12);
        if (state.audio.crackleTimer) { clearInterval(state.audio.crackleTimer); state.audio.crackleTimer = null; }
        soundToggle.textContent = '🔊 开启暖暖声音';
      }
      localStorage.setItem('burn-sound', String(next));
    });

    motionToggle.addEventListener('click', () => {
      const pressed = motionToggle.getAttribute('aria-pressed') === 'true';
      const next = !pressed; state.reducedMotion = next; motionToggle.setAttribute('aria-pressed', String(next)); motionToggle.textContent = next ? '🫧 已减少动态' : '🌿 减少动态'; localStorage.setItem('burn-motion', String(next));
    });

    // Restore prefs
    (function restore(){
      const y = new Date().getFullYear(); $('#year').textContent = y;
      const prefSound = localStorage.getItem('burn-sound'); const prefMotion = localStorage.getItem('burn-motion');
      if (prefMotion === 'true') { motionToggle.click(); }
      if (prefSound === 'true') { soundToggle.click(); }
      if (prefMotion === null && matchMedia('(prefers-reduced-motion: reduce)').matches) { if (motionToggle.getAttribute('aria-pressed') !== 'true') motionToggle.click(); }
    })();

    // --- Minimal test harness (runs only with ?test=1 or #run-tests) ---
    (function testHarness(){
      const qp = new URLSearchParams(location.search);
      const shouldRun = qp.get('test') === '1' || location.hash.includes('run-tests');
      if (!shouldRun) return;
      const results = [];
      const assert = (cond, msg) => { results.push({ ok: !!cond, msg }); };
      try {
        assert(typeof burnWorries === 'function', 'burnWorries 应为函数');
        assert(typeof lettersToCloud === 'function', 'lettersToCloud 应为函数');
        assert(typeof arcCloudToStove === 'function', 'arcCloudToStove 应为函数');
        // Prepare DOM rect
        const r = $('#worries').getBoundingClientRect();
        $('#worries').value = '测试一下小乌云';
        const tmp = lettersToCloud(r);
        assert(tmp && tmp.wrap && tmp.cloud, 'lettersToCloud 返回 wrap 与 cloud');
        // cleanup
        tmp.cloud.remove(); tmp.wrap.remove();
        // Audio init
        initAudio();
        assert(!!state.audio.ctx, '音频上下文已创建');
        // Empty text should not set working
        $('#worries').value = '';
        burnWorries();
        setTimeout(() => {
          assert(state.working === false, '空输入不会进入工作状态');
          // New tests
          assert(Array.isArray(quotes) && quotes.length >= 100, '暖心语不少于 100 条');
          const fs = parseFloat(getComputedStyle($('#quote')).fontSize);
          assert(fs >= 20, '安慰语字号足够醒目');
          assert(state.QUOTE_STAY === 3500, '安慰语停留时间已设为 3.5 秒');
          // hearts & window placement tests
          assert(typeof spawnHearts === 'function', 'spawnHearts 存在');
          const before = document.getElementById('heartsHost').childElementCount; spawnHearts(3);
          assert(document.getElementById('heartsHost').childElementCount >= before, '爱心已生成');
          const win = document.getElementById('window');
          assert(win && /translate\(140,260\)/.test(win.getAttribute('transform')||''), '炉窗位置已下移避免与嘴重叠');
          // 入口点必须位于炉窗内部
          try {
            const { x, y } = getWindowTargetPoint();
            const rr = win.getBoundingClientRect();
            assert(y > rr.top + 0.2*rr.height && y < rr.bottom, '入口点垂直落在炉窗内部');
            assert(x > rr.left && x < rr.right, '入口点水平落在炉窗内部');
          } catch(e) {
            assert(false, '入口点函数应存在并返回数值');
          }
          // sound toggle flips state (no need to resume audio)
          const prev = state.soundOn; soundToggle.click();
          setTimeout(() => {
            assert(state.soundOn !== prev, '声音开关切换状态成功');
            // 静音下音效函数不应抛错
            try { state.soundOn = false; whoosh(); ding(); assert(true, '静音下 whoosh/ding 安全返回'); } catch(e) { assert(false, '静音下 whoosh/ding 不应抛错'); }
            // Show panel
            const panel = document.createElement('div');
            panel.style.cssText = 'position:fixed;right:12px;bottom:12px;background:#fff7ef;border:1px solid #efc99a;border-radius:10px;padding:10px 12px;font:14px/1.4 system-ui;color:#7a5a43;box-shadow:0 8px 18px rgba(0,0,0,.08);z-index:99999;max-width:360px;';
            panel.innerHTML = '<b>自检结果</b><br>' + results.map(r=> (r.ok?'✅ ':'❌ ')+r.msg).join('<br>');
            document.body.appendChild(panel);
          }, 120);
        }, 100);
      } catch(e) {
        console.error('Test harness error', e);
      }
    })();

    // Focus textarea on load for convenience
    window.addEventListener('load', () => textarea.focus());
  </script>
</body>
</html>
```